/*
 * @(#)BasicPlayer.java	1.81 02/08/21
 *
 * Copyright (c) 1996-2002 Sun Microsystems, Inc.  All rights reserved.
 */

package com.sun.media;

import java.awt.Component;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Vector;

import javax.media.AudioDeviceUnavailableEvent;
import javax.media.CachingControl;
import javax.media.CachingControlEvent;
import javax.media.Clock;
import javax.media.ClockStartedError;
import javax.media.Control;
import javax.media.Controller;
import javax.media.ControllerClosedEvent;
import javax.media.ControllerErrorEvent;
import javax.media.ControllerEvent;
import javax.media.ControllerListener;
import javax.media.DownloadProgressListener;
import javax.media.DurationUpdateEvent;
import javax.media.EndOfMediaEvent;
import javax.media.ExtendedCachingControl;
import javax.media.GainControl;
import javax.media.IncompatibleSourceException;
import javax.media.IncompatibleTimeBaseException;
import javax.media.MediaLocator;
import javax.media.NotPrefetchedError;
import javax.media.NotRealizedError;
import javax.media.Player;
import javax.media.Processor;
import javax.media.ResourceUnavailableEvent;
import javax.media.RestartingEvent;
import javax.media.SizeChangeEvent;
import javax.media.StartEvent;
import javax.media.StopAtTimeEvent;
import javax.media.StopByRequestEvent;
import javax.media.StopEvent;
import javax.media.StopTimeChangeEvent;
import javax.media.Time;
import javax.media.TimeBase;
import javax.media.control.BufferControl;
import javax.media.protocol.Positionable;
import javax.media.protocol.RateConfiguration;
import javax.media.protocol.RateConfigureable;
import javax.media.protocol.RateRange;

import com.ms.security.PermissionID;
import com.ms.security.PolicyEngine;
import com.sun.media.controls.SliderRegionControl;
import com.sun.media.controls.SliderRegionControlAdapter;
import com.sun.media.ui.DefaultControlPanel;
import com.sun.media.util.JMFI18N;
import com.sun.media.util.LoopThread;
import com.sun.media.util.MediaThread;
import com.sun.media.util.jdk12;

/**
 * BasicPlayer implements the bases of a javax.media.Player.  It handles
 * all the Player state transitions, event handling and management of
 * any Controller under its control.
 */

public abstract class BasicPlayer extends BasicController
	implements Player, ControllerListener, DownloadProgressListener {

    static public String VERSION = JMFI18N.getResource("mediaplayer.version");

    //    private MediaSource source = null;
    protected javax.media.protocol.DataSource source = null;
    protected Vector controllerList = new Vector();
    private Vector optionalControllerList = new Vector();
    private Vector removedControllerList = new Vector();
    private Vector currentControllerList = new Vector();
    private Vector potentialEventsList = null;
    private Vector receivedEventList = new Vector();
    private boolean receivedAllEvents = false;
    private Vector configureEventList = new Vector();
    private Vector realizeEventList = new Vector();
    private Vector prefetchEventList = new Vector();
    private Vector stopEventList = new Vector();
    private ControllerEvent CachingControlEvent = null;
    private Controller restartFrom = null;
    private Vector eomEventsReceivedFrom = new Vector();
    private Vector stopAtTimeReceivedFrom = new Vector();
    private PlayThread playThread = null;
    private StatsThread statsThread = null;
    private Time duration = DURATION_UNKNOWN;
    private Time startTime, mediaTimeAtStart;
    private boolean aboutToRestart = false;
    private boolean closing = false;
    private boolean prefetchFailed = false;
    protected boolean framePositioning = true;	    
	    

    protected Control [] controls = null;
    protected Component controlComp = null;

    // Information controls
    public SliderRegionControl regionControl = null;
    
    protected CachingControl cachingControl = null;
    protected ExtendedCachingControl extendedCachingControl = null;
    protected BufferControl bufferControl = null;

    private Object startSync = new Object();
    private Object mediaTimeSync = new Object();

    private static JMFSecurity jmfSecurity = null;
    private static boolean securityPrivelege=false;
    private Method m[] = new Method[1];
    private Class cl[] = new Class[1];
    private Object args[][] = new Object[1][0];

    static {
	try {
	    jmfSecurity = JMFSecurityManager.getJMFSecurity();
	    securityPrivelege = true;
	} catch (SecurityException e) {
	}
    }

    public BasicPlayer() {
 	configureEventList.addElement("javax.media.ConfigureCompleteEvent");
 	configureEventList.addElement("javax.media.ResourceUnavailableEvent");

 	realizeEventList.addElement("javax.media.RealizeCompleteEvent");
 	realizeEventList.addElement("javax.media.ResourceUnavailableEvent");

 	prefetchEventList.addElement("javax.media.PrefetchCompleteEvent");
 	prefetchEventList.addElement("javax.media.ResourceUnavailableEvent");

	stopEventList.addElement("javax.media.StopEvent");
	stopEventList.addElement("javax.media.StopByRequestEvent");
	stopEventList.addElement("javax.media.StopAtTimeEvent");
	stopThreadEnabled = false;
    }

    /**
     * Will return true if the player can do frame
     * positioning. Hack for now, should be removed when players
     * actually implement the framePositioning control.
     */
    public boolean isFramePositionable(){
	return framePositioning;
    }
	    
    /**
     * A player is not configurable.
     */
    protected boolean isConfigurable() {
	return false;
    }

    /**
     * Set the DataSource that provides the media for this player.
     * BasicPlayer only supports PullDataSource by default.  Subclasses
     * can override this method to support other DataSources. 
     *
     * @param source of media for this player.
     * @exception IOException thrown when an i/o error occurs
     * in reading information from the data source.
     * @exception IncompatibleSourceException thrown if the Player 
     * can't use this source.
     */
    public void setSource(javax.media.protocol.DataSource source)
	throws IOException, IncompatibleSourceException {
	this.source = source;

        try {
	    cachingControl = (CachingControl) source.getControl("javax.media.CachingControl");
	    if ( (cachingControl != null) &&
		 (cachingControl instanceof ExtendedCachingControl) ) {
		    extendedCachingControl = (ExtendedCachingControl) cachingControl;
		    if (extendedCachingControl != null) {
			// update progress every 100 kilobytes
			regionControl = new SliderRegionControlAdapter();
			extendedCachingControl.addDownloadProgressListener(this, 100);
		    }
	    }
        } catch (ClassCastException e) {
        }
    }

    public void downloadUpdate() {
        if (extendedCachingControl == null)
            return;

        // It will be nice if we can avoid the cast to BasicController
	sendEvent(new CachingControlEvent(this, cachingControl,
                                          cachingControl.getContentProgress()));

        if ( regionControl == null )
            return;

        long contentLength = cachingControl.getContentLength();
        int maxValuePercent;
        if ( (contentLength == javax.media.protocol.SourceStream.LENGTH_UNKNOWN) ||
             (contentLength <= 0) ) {
            maxValuePercent = 0;
        } else {
            long endOffset = extendedCachingControl.getEndOffset();
            maxValuePercent = (int) ((100.0 * endOffset) / contentLength);
            if (maxValuePercent < 0)
                maxValuePercent = 0;
            else if (maxValuePercent > 100)
                maxValuePercent = 100;
        }
        regionControl.setMinValue(0);
        regionControl.setMaxValue(maxValuePercent);
    }

    public MediaLocator getMediaLocator() {
	if (source != null)
	    return source.getLocator();
	return null;
	    
    }

    public String getContentType() {
	if (source != null)
	    return source.getContentType();
	return null;
	    
    }

    /**
     * Get the DataSource used by this player.
     * @return the DataSource used by this player.
     */
    protected javax.media.protocol.DataSource getSource() {
	return source;
    }

    /**
     * This is called when close() is invoked on the Player.  close() takes
     * care of the general behavior before invoking doClose().  Subclasses 
     * should implement this only if it needs to do something specific to 
     * close the player.
     */
    protected void doClose() {

	synchronized (this) {
	    closing = true;
	    notifyAll();
	}

	if (getState() == Controller.Started) {
	    // Stop everything first.
	    stop(LOCAL_STOP);
	}

	// Ask all its controllers to close themselves.
	if (controllerList != null) {
	    Controller c;
	    while (!controllerList.isEmpty()) {
		c = (Controller)controllerList.firstElement();
		c.close();
		controllerList.removeElement(c);
	    }
	}

	// Close the ui components.
	if (controlComp != null)
	    ((DefaultControlPanel)controlComp).dispose();
	controlComp = null;

	if (statsThread != null)
	    statsThread.kill();

	sendEvent(new ControllerClosedEvent(this));
    }

    /**
     * Assigns a timebase for the player.
     * If the BasicPlayer plays back audio, the timebase can be none
     * other than the master timebase as returned by getMasterTimeBase().
     * This is to ensure that we won't set to a timebase the audio
     * cannot handle at this point.
     * If the playback is video only, the timebase can be set to any
     * timebase desired.
     * @param tb time base to be used by the Player.
     * @exception IncompatibleTimeBaseException thrown when a time base other
     *  than the master time base is set when audio is enabled.
     */
    public void setTimeBase(TimeBase tb) throws IncompatibleTimeBaseException{

	TimeBase oldTimeBase = getMasterTimeBase();

	if (tb == null)
	    tb = oldTimeBase;

	Controller c = null;
	int i;

	if (controllerList != null) {
	    try {
		i = controllerList.size();
		while (--i >= 0) {
		    c = (Controller)controllerList.elementAt(i);
		    c.setTimeBase(tb);
		}
	    } catch (IncompatibleTimeBaseException e) {

		// SetTimeBase had failed on one Controller.  Some
		// controllers may have already assigned the new timeBase
		// We'll need to reverse that now.
		Controller cx;
		i = controllerList.size();
		while (--i >= 0) {
		    cx = (Controller)controllerList.elementAt(i);
		    if (cx == c)
			break;
		    cx.setTimeBase(oldTimeBase);
		}
		Log.dumpStack(e);
		throw e;
	    }
	}

	super.setTimeBase(tb);
    }
    
    /**
     * Set the upper bound of the media time.
     * @param duration the duration in nanoseconds.
     */
    protected void setMediaLength(long t) {
	duration = new Time(t);
	super.setMediaLength(t);
    }


    long lastTime = 0;
    
    /**
     * Get the duration of the movie.
     * @return the duration of the movie.
     */
    public Time getDuration() {
	long t;
	if ((t = getMediaNanoseconds()) > lastTime) {
	    lastTime = t;
	    updateDuration();
	}
	return duration;
    }

    protected synchronized void updateDuration() {
	Time oldDuration = duration;
	duration = DURATION_UNKNOWN;
	for (int i = 0; i < controllerList.size(); i++) {
	    Controller c = (Controller) controllerList.elementAt(i);
	    Time dur = c.getDuration();
	    if (  dur.equals(DURATION_UNKNOWN) ) {
		if (! (c instanceof BasicController)) {
		    duration = DURATION_UNKNOWN;
		    break;
		}
	    } else if (dur.equals(DURATION_UNBOUNDED)) {
		duration = DURATION_UNBOUNDED;
		break;
	    } else {
		if (duration.equals(DURATION_UNKNOWN))
		    duration = dur;
		else if (duration.getNanoseconds() < dur.getNanoseconds())
		    duration = dur;
	    }
	}
	if (duration.getNanoseconds() != oldDuration.getNanoseconds()) {
	    setMediaLength(duration.getNanoseconds());
	    sendEvent( new DurationUpdateEvent(this, duration) );
	}
    }
    
    public Time getStartLatency() {
	super.getStartLatency();

	Time latency;
	long t = 0;

	// Find the longest start latency from all the slave Controllers.
	for (int i = 0; i < controllerList.size(); i++) {
	    Controller c = (Controller) controllerList.elementAt(i);
	    latency = c.getStartLatency();

	    if (latency == LATENCY_UNKNOWN)
		continue;

	    if (latency.getNanoseconds() > t)
		t = latency.getNanoseconds();
	}

	if (t == 0)
	    return LATENCY_UNKNOWN;
	
	return new Time(t);
    }

    /**
      * Stop because stop time has been reached.
      * Subclasses should override this method.
      */
    protected void stopAtTime() {
	// We'll overwrite the parent method which stops the controller.
	// For the player, we don't have to do anything in particular
	// since the controllers are supposed to stop themselves.
    }

    /**
     * This is for subclass to access Controller's implementation of stopAtTime.
     */
    protected void controllerStopAtTime() {
	super.stopAtTime();
    }

    public void setStopTime(Time t) {
	if (state < Realized) {
	    throwError(new NotRealizedError("Cannot set stop time on an unrealized controller."));
	}

	if (getClock().getStopTime() == null ||
	    getClock().getStopTime().getNanoseconds() != t.getNanoseconds()) {
	    sendEvent(new StopTimeChangeEvent(this, t));
	}

	doSetStopTime(t);
    }

    private void doSetStopTime(Time t) {
	getClock().setStopTime(t);
	Vector list = controllerList;
        int i = list.size();
        while (--i >= 0) {
	    Controller c = (Controller)controllerList.elementAt(i);
	    c.setStopTime(t);
	}
    }

    /**
     * This is for subclass to access Controller's implementation of setStopTime.
     */
    protected void controllerSetStopTime(Time t) {
	super.setStopTime(t);
    }

    /**
     * Loops through the list of controllers maintained by this
     * player and invoke setMediaTime on each of them.
     * This is a "final" method and cannot be overridden by subclasses.
     * @param now the target media time.
     **/
    public final void setMediaTime(Time now) {

	if (state < Realized)
	    throwError(new NotRealizedError(MediaTimeError));

	// Set Media time from EOM and user click on the slider is
	// trampling on one another.  Causing the player to hang
	// this mediaTimeSync will guard against that.
      synchronized (mediaTimeSync) {

	if (syncStartInProgress())
	    return;

	if (getState() == Controller.Started) {
	    aboutToRestart = true;
	    stop(RESTARTING);
	}

	// If source is Positionable, we'll take care of this
	// at the top level.
	if (source instanceof Positionable)
	    now = ((Positionable)source).setPosition(now, Positionable.RoundDown);

	super.setMediaTime(now);

        int i = controllerList.size();
        while (--i >= 0) {
            ((Controller)controllerList.elementAt(i)).setMediaTime(now);
	}

	// For subclasses to add in their own behavior.
	doSetMediaTime(now);

	if (aboutToRestart) {
	    syncStart(getTimeBase().getTime());
	    aboutToRestart = false;
	}
      }
    }

    /**
     * Return true if the player is about to restart again.
     */
    public boolean isAboutToRestart() {
	return aboutToRestart;
    }

    /**
     * Called from setMediaTime.
     * This is used for subclasses to add in their own behavior.
     * @param now the target media time.
     */
    protected void doSetMediaTime(Time now) {
    }

    /**
     * Get the Component this player will output its visual media to.  If
     * this player has no visual component (e.g. audio only)
     * getVisualComponent() will return null.
     * Subclasses should override this method and return the visual
     * component but call this method first to ensure that the restrictions
     * on player methods are enforced.
     * @return the media display component.
    */
    public Component getVisualComponent() {
	int state = getState();
	if (state < Realized) {
	    throwError(new NotRealizedError("Cannot get visual component on an unrealized player"));
	}
	return null;
    }

    /**
     * Get the Component with the default user interface for controlling
     * this player.
     * If this player has no default control panel null is
     * returned.
     * Subclasses should override this method and return the control panel
     * component but call this method first to ensure that the restrictions
     * on player methods are enforced.
     * @return the default control panel GUI.
     */
    public Component getControlPanelComponent() {
	int state = getState();
	if (state < Realized) {
	    throwError(new NotRealizedError("Cannot get control panel component on an unrealized player"));
	}
	if (controlComp == null) {
	  controlComp = new DefaultControlPanel( this );
	}
	return controlComp;
    }

    /**
     * Get the object for controlling audio gain. Return null
     * if this player does not have a GainControl (e.g. no audio).
     *
     * @return the GainControl object for this player.
    */
    public GainControl getGainControl() {
	int state = getState();
	if (state < Realized) {
	    throwError(new NotRealizedError("Cannot get gain control on an unrealized player"));
	} else {
	    return (GainControl)getControl("javax.media.GainControl");
	}
	return null;
    }

    /**
     * Return the list of controls from its slave controllers plus the
     * ones that this player supports.
     * @return the list of controls supported by this player.
     */
    public Control [] getControls() {
	if (controls != null)
	    return controls;

	// build the list of controls.  It is the total of all the
	// controls from each controllers plus the ones that are maintained
	// by the player itself (e.g. playbackControl).

	Vector cv = new Vector();

	if (cachingControl != null)
	    cv.addElement(cachingControl);

	if (bufferControl != null)
	    cv.addElement(bufferControl);

	Control c;
	Object cs[];
	Controller ctrller;
	int i, size = controllerList.size();
	for (i = 0; i < size; i++) {
	    ctrller = (Controller)controllerList.elementAt(i);
	    cs = ctrller.getControls();
	    if (cs == null) continue;
	    for (int j = 0; j < cs.length; j++) {
		cv.addElement(cs[j]);
	    }
	}

	Control ctrls[];
	size = cv.size();
	ctrls = new Control[size];

	for (i = 0; i < size; i++) {
	    ctrls[i] = (Control)cv.elementAt(i);
	}

	// If the player has already been realized, we'll save what
	// we've collected this time.  Then next time, we won't need
	// to go through this expensive search again. 
	if (getState() >= Realized)
	    controls = ctrls;

	return ctrls;
    }
    
    /**
     * This get called when some Controller notifies this player of 
     * any event. 
     */
    final public void controllerUpdate(ControllerEvent evt) {
	processEvent(evt);
    }

    /**
     * Return the list of BasicControllers supported by this Player.
     * @return a vector of the BasicControllers supported by this Player.
     */ 
    public final Vector getControllerList() {
	return controllerList;
    }

    private Vector getPotentialEventsList() {
	return potentialEventsList;
    }

    /**
     * Resets the list of received events
     */
    private void resetReceivedEventList() {
	if (receivedEventList != null)
	    receivedEventList.removeAllElements();
    }

    /**
     * Return the list of received events
     */
    private Vector getReceivedEventsList() {
	return receivedEventList;
    }

    /**
     * Updates the list of received events. Sources are stored.
     */
    private void updateReceivedEventsList(ControllerEvent event) {
	if (receivedEventList != null) {
	    Controller source = event.getSourceController();

	    if (receivedEventList.contains(source)) {
		// System.out.println("DUPLICATE " + event +  
		//	" received from: " + source);
		return;
	    }
	    receivedEventList.addElement(source);
	}
    }

    /**
     * Start the Player as soon as possible.
     * Start attempts to transition the player into the
     * started state.
     * If the player has not been realized, or prefetched,
     * then the equivalent of those actions will occur,
     * and the appropriate events will be generated.
     * If the implied realize or prefetch fail, a failure
     * event will be generated and the Player will remain in
     * one of the non-started states.<p>
     * This is a "final" method.  Subclasses should override doStart() to
     * implement its own specific behavior.
     */
    public final void start() {

      synchronized (startSync) {

	if (restartFrom != null) {
	    return;
	}

	if (getState() == Started) {
	    sendEvent(new StartEvent(this, Started, Started,
				     Started, mediaTimeAtStart, startTime));
	    return; // ignored according to jmf spec.
	}
	if ( (playThread == null) || (! playThread.isAlive()) ) {
	    setTargetState(Started);
	    if ( /*securityPrivelege && */ (jmfSecurity != null) ) {
		String permission = null;
		try {
		    if (jmfSecurity.getName().startsWith("jmf-security")) {
			permission = "thread";
			jmfSecurity.requestPermission(m, cl, args, JMFSecurity.THREAD);
			m[0].invoke(cl[0], args[0]);
			
			permission = "thread group";
			jmfSecurity.requestPermission(m, cl, args, JMFSecurity.THREAD_GROUP);
			m[0].invoke(cl[0], args[0]);
		    } else if (jmfSecurity.getName().startsWith("internet")) {
			PolicyEngine.checkPermission(PermissionID.THREAD);
			PolicyEngine.assertPermission(PermissionID.THREAD);
		    }
		} catch (Throwable e) {
		    if (JMFSecurityManager.DEBUG) {
			System.err.println("Unable to get " + permission +
					   " privilege  " + e);
		    }
		    securityPrivelege = false;
		    // TODO: Do the right thing if permissions cannot be obtained.
		    // User should be notified via an event
		}
	    }

 	    if ( (jmfSecurity != null) && (jmfSecurity.getName().startsWith("jdk12"))) {
		try {
		    Constructor cons = CreateWorkThreadAction.cons;
		    playThread = (PlayThread) jdk12.doPrivM.invoke(
                                           jdk12.ac,
 					  new Object[] {
 					  cons.newInstance(
 					   new Object[] {
                                               PlayThread.class,
					       BasicPlayer.class,
                                               this
                                           })});

		    playThread.start();
		} catch (Exception e) {
		}
	    } else {
		playThread = new PlayThread(this);
		System.out.println("Waiting for PlayThread..");
		playThread.start();
		System.out.println("PlayThread running...");
	    }

	} else {
	    // $$$$
	    // 	    System.out.print("WARNING: playThread is alive. start ignored"); // $$$
	    // 	    System.out.println(": MP State: " + getState());
	}

      } // startSync
    }

    /**
     * Start at the given time base time.
     * This overrides Clock.syncStart() and obeys all its semantics.<p>
     * This is a "final" method.  Subclasses should override doStart() to
     * implement its own specific behavior.
     * @param tbt the time base time to start the player.
     */
    public final void syncStart(Time tbt) {

      /**
       * To guard against conflict with setMediaTime.
       */
      synchronized (mediaTimeSync) {

	if (syncStartInProgress())
	    return;

	int state = getState();

	if (state == Started) {
	    throwError(new ClockStartedError("syncStart() cannot be used on an already started player"));
	}

	if (state != Prefetched) {
	    throwError(new NotPrefetchedError("Cannot start player before it has been prefetched"));
	}

	// Clear the EOM and StopAtTime lists.
	eomEventsReceivedFrom.removeAllElements();
	stopAtTimeReceivedFrom.removeAllElements();

	setTargetState(Started);

	int i = controllerList.size();
	// The start(tbt) will throw a NotPrefetchedError if
	// a controller is not in Prefetched state
	while (--i >= 0) {
	    if (getTargetState() == Started) {  // ADDED
		((Controller)controllerList.elementAt(i)).syncStart(tbt);
	    }
	}

	if (getTargetState() == Started) {  // ADDED
	    // If control comes here, the controllers
	    // are in Started state.
	    startTime = tbt;
	    mediaTimeAtStart = getMediaTime();
	    super.syncStart(tbt); // To start the clock and set the state to Started
	}
      }
    }

    /**
     * Invoked by start() or syncstart().
     * Called from a separate thread called TimedStart thread.
     * subclasses can override this method to implement its specific behavior.
     */
    protected void doStart() {
     }
    
    /**
     * This method gets run in a separate thread called PlayThread
     */
    final synchronized void play() {
	boolean status;

	// If a deallocate() happens before this thread gets to run.
	// or if a stop() happens before this thread gets to run.
	if (getTargetState() != Started) {
	    return;
	}

	prefetchFailed = false;

	// The following completed checks should be looked at seriously.
	// It should be something like (state < Prefetched)  etc.
	// It's too late for 2.1.1 release.  We'll leave it this way.  --ivg

	int state = getState();
	System.out.println("State: "+state);
	if ( (state == Unrealized) || (state == Configured) || (state == Realized) ) {
	    prefetch();
	}
	while (!closing && !prefetchFailed &&
		(getState() == Configuring || getState() == Realizing || 
		 getState() == Realized || getState() == Prefetching)) {
	    try {
		wait();
	    } catch (InterruptedException e) {
	    }
	}

	if ( getState() != Started &&
	     getTargetState() == Started && getState() == Prefetched ) {
	    syncStart(getTimeBase().getTime());
	}
    }

    /**
     * The stop() method calls doStop() so that subclasses can
     * add additional behavior.
     */
    protected void doStop() {
    }

    /**
     * Stop the player.
     * If current state is Started, sends stop() to all the
     * managed controllers, and waits for a StopEvent from
     * all of them. It then sends a StopEvent to any listener(s).
     */
    public final /*synchronized*/ void stop() {
	stop(STOP_BY_REQUEST);
    }

    static final int LOCAL_STOP = 0;
    static final int STOP_BY_REQUEST = 1;
    static final int RESTARTING = 2;

    private /*synchronized*/ void stop(int stopType) {

	int state;

	switch (state = getState()) {
	case Unrealized:
	case Realized:
	case Prefetched:
	    setTargetState(state);
	    break;
	case Realizing:
	    setTargetState(Realized);
	    break;
	case Prefetching:
	case Started:
	    setTargetState(Prefetched);
	    break;
	}

	if (getState() != Started) {
	    switch (stopType) {
	    case STOP_BY_REQUEST: 
		sendEvent( new StopByRequestEvent(this, getState(),
					      getState(),
					      getTargetState(),
					      getMediaTime()));
		break;
	    case RESTARTING:
		sendEvent( new RestartingEvent(this, getState(),
					      getState(),
					      Started,
					      getMediaTime()));
		break;
	    default:
		sendEvent( new StopEvent(this, getState(),
					      getState(),
					      getTargetState(),
					      getMediaTime()));
		break;
	    }

	} else if (getState() == Started) {

	  synchronized(this) {
	    // List of potential events for stop()
	    potentialEventsList = stopEventList; 
	    // Reset list of received events
	    resetReceivedEventList();
	    receivedAllEvents = false;
	    currentControllerList.removeAllElements();

	    int i = controllerList.size();
	    while (--i >= 0) {
		Controller c = (Controller) controllerList.elementAt(i);
		currentControllerList.addElement(c);
		c.stop();
	    }
	    if (currentControllerList == null)
		return;
	    if (!currentControllerList.isEmpty()) {
		try {
		    while (!closing && !receivedAllEvents)
			wait();
		} catch (InterruptedException e) {
		}
		currentControllerList.removeAllElements();
	    }
	    super.stop();
	    //doStop(); // Allow subclasses to extend the behavior

	    switch (stopType) {
	    case STOP_BY_REQUEST: 
		sendEvent( new StopByRequestEvent(this, Started,
					      getState(),
					      getTargetState(),
					      getMediaTime()));
		break;
	    case RESTARTING:
		sendEvent( new RestartingEvent(this, Started,
					      getState(),
					      Started,
					      getMediaTime()));
		break;
	    default:
		sendEvent( new StopEvent(this, Started,
					      getState(),
					      getTargetState(),
					      getMediaTime()));
		break;
	    }
	  }
	}
    }

    protected final void processEndOfMedia() {
	super.stop();
 	sendEvent(new EndOfMediaEvent(this, Started, Prefetched,
 				      getTargetState(), getMediaTime()));
    }

    /**
     * Add a Controller to the list of Controllers under this Player's
     * management.  This is a protected method use only by subclasses.
     * Use addController() for public access.
     */
    protected final void manageController(Controller controller) {
	manageController(controller, false);
    }

    /**
     * Add a Controller to the list of Controllers under this Player's
     * management.
     */
    protected final void manageController(Controller controller, boolean optional) {
	if (controller != null) {
	    if (!controllerList.contains(controller)) {
		controllerList.addElement(controller);
		if (optional)
		  optionalControllerList.addElement(controller);
		controller.addControllerListener(this);
	    }
	}
	updateDuration();
    }

    /**
     * Remove a Controller from the list of Controllers under this Player's
     * management.  This is a protected method use only by subclasses.
     * Use removeController() for public access.
     */
    public final void unmanageController(Controller controller) {
	if (controller != null)
	    if (controllerList.contains(controller)) {
		controllerList.removeElement(controller);
		controller.removeControllerListener(this);
	    }
    }

    /**
     * Assume control of another Controller.
     * A Player can accept responsibility for controlling
     * another Controller.
     * Once a Controller has been added
     * this Player will:
     * <ul>
     * <li> Slave the Controller to the Player's time-base.
     * <li> Use the Controller in the Player's computation 
     * of start latency.
     * The value the Player returns in its <b>getStartLatency</b> 
     * method is the larger
     * of:  <b>getStartLatency</b> before the Controller was added, or
     * <b>getStartLatency</b> of the Controller.
     * <li> Pass along, as is appropriate, events that the Controller
     * generates.
     * <li> Invoke all Controller methods on the Controller.
     * <li> For all asynchronous methods (realize, prefetch) a completion
     * event will not be generated until all added Controllers have 
     * generated completion events.
     * </ul><p>
     *
     * <b>Note:</b> It is undefined what will happen if a Controller is
     * under the control of a Player and any of the 
     * Controller's methods are called outside of the controlling 
     * Player.
     *
     * @param newController the Controller this
     * Player will control.
     * @exception IncompatibleTimeBaseException thrown if the new controller 
     * will not accept the player's timebase.
     */
    public synchronized void addController(Controller newController)
	throws IncompatibleTimeBaseException
    {
	int playerState = getState();
	
	if (playerState == Started) {
	    throwError(new ClockStartedError("Cannot add controller to a started player"));
	}
	
	if ( (playerState == Unrealized) || (playerState == Realizing) ) {
	    throwError(new NotRealizedError("A Controller cannot be added to an Unrealized Player"));
	}

	if (newController == null || newController == this)
	    return;

	int controllerState = newController.getState();
	if ( (controllerState == Unrealized) || (controllerState == Realizing) ) {
	    throwError(new NotRealizedError("An Unrealized Controller cannot be added to a Player"));
	}

 	if (controllerList.contains(newController)) {
	    return;
	}

	if (playerState == Prefetched) {
	    if ( (controllerState == Realized) ||
		 (controllerState == Prefetching) ) {
		// System.out.println("Calling deallocate");
		deallocate(); // Transition back to realized state
	    }
	}

	manageController(newController);

	// Synchronize the players.
	newController.setTimeBase(getTimeBase());
	newController.setMediaTime(getMediaTime());
	newController.setStopTime(getStopTime());

	if (newController.setRate(getRate()) != getRate()) {
	    // The slave does not support the master's rate.
	    // We'll reset everything back to rate 1.0.
	    setRate(1.0f);
	}
    }

    /**
     * Stop controlling a Controller.
     *
     * @param oldController the Controller to stop controlling.
     */
    public final synchronized void removeController(Controller oldController) {
	int state = getState();

	if (state < Realized) {
	    throwError(new NotRealizedError("Cannot remove controller from a unrealized player"));
	}

	if (state == Started) {
	    throwError(new ClockStartedError("Cannot remove controller from a started player"));
	}

	if (oldController == null)
	    return;


 	if (controllerList.contains(oldController)) {
 	    controllerList.removeElement(oldController);
	    oldController.removeControllerListener(this);
	    updateDuration();
	    // Reset the controller to its default time base.
	    try {
		oldController.setTimeBase(null);
	    } catch (IncompatibleTimeBaseException e) {}
	}
    }

    /**
     * Return true if the player is currently playing media 
     * with an audio track.
     * @return true if the player is playing audio.
     */
    protected abstract boolean audioEnabled();

    /**
     * Return true if the player is currently playing media 
     * with a video track.
     * @return true if the player is playing video.
     */
    protected abstract boolean videoEnabled();

    /**
     * This should be implemented by the subclass.
     * The subclass method should return the master TimeBase -- the
     * TimeBase that all other controllers slave to.
     * Use SystemTimeBase if unsure.
     * @return the master time base.
     */
    protected abstract TimeBase getMasterTimeBase();

    /**
     * The stub function (invoked from configure()) to perform the steps to 
     * configure the player.  It performs the following:
     * <ul>
     * <li> call configure() on each controller managed by this player. 
     * <li> wait for ConfigureCompleteEvent from each controller;
     * </ul>
     * Subclasses are allowed to override doConfigure().  But this should be
     * done in caution.  Subclass should also invoke super.doConfigure(). 
     * This is called from a separately running thread.
     * @return true if successful.
     */ 
    protected synchronized boolean doConfigure() {

	potentialEventsList = configureEventList; // List of potential events for the
	                                        // configure() method
	resetReceivedEventList(); 		// Reset list of received events
	receivedAllEvents = false;
	currentControllerList.removeAllElements();

	int i = controllerList.size();
	while (--i >= 0) {
	    Controller c = (Controller) controllerList.elementAt(i);
	    if (c.getState() == Unrealized && 
		(c instanceof Processor || c instanceof BasicController)) {
		currentControllerList.addElement(c);
	    }
	}

	i = currentControllerList.size();
	while (--i >= 0) {
	    Controller c = (Controller) currentControllerList.elementAt(i);
	    if (c instanceof Processor)
		((Processor)c).configure();
	    else if (c instanceof BasicController)
		((BasicController)c).configure();
	}

	if (!currentControllerList.isEmpty()) {
	    try {
		while (!closing && !receivedAllEvents)
		    wait();
	    } catch (InterruptedException e) {
	    }
	    currentControllerList.removeAllElements();
	}

	// Make sure all the controllers are in in Configured State.
	// If not, it means that configure failed on one or more controllers.
	// Currenly, if configure fails then you get a ResourceUnavailableEvent
	// instead of RealizeCompleteEvent

	i = controllerList.size();
	while (--i >= 0) {
	    Controller c = (Controller) controllerList.elementAt(i);
	    if ((c instanceof Processor || c instanceof BasicController) &&
		c.getState() < Configured) {
		Log.error("Error: Unable to configure " + c);
		source.disconnect();
		return false;
	    }
	}

	// subclass will implement this to configure up the signal graph.
	return true;
    }

    /**
     * Called as a last step to complete the configure call.
     */
    protected void completeConfigure() {
	super.completeConfigure();
	synchronized (this) {
	    notify();
	}
    }

    /**
     * Called when configure fails.
     */
    protected void doFailedConfigure() {
	super.doFailedConfigure();
	synchronized (this) {
	    notify();
	}
	close();
    }

    /**
     * The stub function (invoked from configure()) to perform the steps to 
     * configure the player.  It performs the following:
     * <ul>
     * <li> call realize() on each controller managed by this player. 
     * <li> wait for RealizeCompleteEvent from each controller;
     * </ul>
     * Subclasses are allowed to override doRealize().  But this should be
     * done in caution.  Subclass should also invoke super.doRealize(). 
     * This is called from a separately running thread.
     * @return true if successful.
     */ 
    protected synchronized boolean doRealize() {
	potentialEventsList = realizeEventList; // List of potential events for the
	                                        // realize() method
	resetReceivedEventList(); 		// Reset list of received events
	receivedAllEvents = false;
	currentControllerList.removeAllElements();

	int i = controllerList.size();
	while (--i >= 0) {
	    Controller c = (Controller) controllerList.elementAt(i);
	    if (c.getState() == Unrealized || c.getState() == Configured) {
		currentControllerList.addElement(c);
	    }
	}

	i = currentControllerList.size();
	while (--i >= 0) {
	    Controller c = (Controller) currentControllerList.elementAt(i);
	    c.realize();
	}

	if (!currentControllerList.isEmpty()) {
	    try {
		while (!closing && !receivedAllEvents)
		    wait();
	    } catch (InterruptedException e) {
	    }
	    currentControllerList.removeAllElements();
	}

	// Make sure all the controllers are in in Realized State.
	// If not, it means that realize failed on one or more controllers.
	// Currenly, if realize fails then you get a ResourceUnavailableEvent
	// instead of RealizeCompleteEvent

	i = controllerList.size();
	while (--i >= 0) {
	    Controller c = (Controller) controllerList.elementAt(i);
	    if (c.getState() < Realized) {
		Log.error("Error: Unable to realize " + c);
		source.disconnect();
		return false;
	    }
	}

	updateDuration();

	if ( /*securityPrivelege &&*/ (jmfSecurity != null) ) {
	    String permission = null;
	    try {
		if (jmfSecurity.getName().startsWith("jmf-security")) {
		    permission = "thread";
		    jmfSecurity.requestPermission(m, cl, args, JMFSecurity.THREAD);
		    m[0].invoke(cl[0], args[0]);
		    
		    permission = "thread group";
		    jmfSecurity.requestPermission(m, cl, args, JMFSecurity.THREAD_GROUP);
		    m[0].invoke(cl[0], args[0]);
		} else if (jmfSecurity.getName().startsWith("internet")) {
		    PolicyEngine.checkPermission(PermissionID.THREAD);
		    PolicyEngine.assertPermission(PermissionID.THREAD);
		}
	    } catch (Exception e) {
		if (JMFSecurityManager.DEBUG) {
		    System.err.println("Unable to get " + permission +
				       " privilege  " + e);
		}
		securityPrivelege = false;
		// TODO: Do the right thing if permissions cannot be obtained.
		// User should be notified via an event
	    }
	}

 	    if ( (jmfSecurity != null) && (jmfSecurity.getName().startsWith("jdk12"))) {
		try {
		    Constructor cons = CreateWorkThreadAction.cons;
		    statsThread = (StatsThread) jdk12.doPrivM.invoke(
                                           jdk12.ac,
 					  new Object[] {
 					  cons.newInstance(
 					   new Object[] {
                                               StatsThread.class,
					       BasicPlayer.class,
                                               this
                                           })});

		    statsThread.start();

	    } catch (Exception e) {
	    }
	} else {
	    statsThread = new StatsThread(this);
	    statsThread.start();
 	}

	// subclass will implement this to connect up the signal graph.
	return true;
    }

    /**
     * Called as a last step to complete the realize call.
     */
    protected void completeRealize() {
	state = Realized;
	try {
	    slaveToMasterTimeBase(getMasterTimeBase());
	} catch (IncompatibleTimeBaseException e) {
	    Log.error(e);
	}
	super.completeRealize();
	synchronized (this) {
	    notify();
	}
    }

    /**
     * Called when realize fails.
     */
    protected void doFailedRealize() {
	super.doFailedRealize();
	synchronized (this) {
	    notify();
	}
	close();
    }

    /**
     * Called as a last step to complete the prefetch call. 
     */
    protected void completePrefetch() {
	super.completePrefetch();
	synchronized(this) {
	    notify();
	}
    }

    /**
     * Called when prefetch fails.
     */
    protected void doFailedPrefetch() {
	super.doFailedPrefetch();
	synchronized (this) {
	    notify();
	}
    }

    /**
     * Called when the realize() is aborted, i.e. deallocate() was called
     * while realizing.  Release all resources claimed previously by the
     * realize() call.
     */
    protected final void abortRealize() {
	if (controllerList != null) {
	    int i = controllerList.size();
	    while (--i >= 0) {
		Controller c = (Controller) controllerList.elementAt(i);
		c.deallocate();
	    }
	}
	synchronized(this) {
	    notify();
	}
    }

    /**
     * The stub function to perform the steps to prefetch the controller.
     * This will call prefetch() on every controller in the controller list and wait
     * for their completion events.
     * This is called from a separately running thread.
     * @return true if successful.
     */ 
    protected  /*synchronized*/ boolean doPrefetch() {

	potentialEventsList = prefetchEventList; // List of potential events for the
	                                        // prefetch() method
	resetReceivedEventList(); 		// Reset list of received events
	receivedAllEvents = false;
	currentControllerList.removeAllElements();

	Vector list = controllerList;

	if (list == null) {
	    return false;
	}

	int i = list.size();
	while (--i >= 0) {
	    Controller c = (Controller) list.elementAt(i);
	    if (c.getState() == Realized) {
		currentControllerList.addElement(c);
		c.prefetch();
	    }
	}
	if (!currentControllerList.isEmpty()) {
	    synchronized(this) {
	        try {
		    while (!closing && !receivedAllEvents)
			wait();
	        } catch (InterruptedException e) {
	        }
	        currentControllerList.removeAllElements();
	    }
	}

	// Make sure all the controllers are in in Prefetched State.
	// If not, it means that prefetch failed on one or more controllers.
	// Currenly, if prefetch fails then you get a ResourceUnavailableEvent
	// instead of PrefetchCompleteEvent

	i = list.size();
	while (--i >= 0) {
	    Controller c = (Controller) list.elementAt(i);
	    if (c.getState() < Prefetched) {
		Log.error("Error: Unable to prefetch " + c + "\n");
		if (optionalControllerList.contains(c)) {
		  //System.out.println(c + " Controller is optional... continuing");
		  removedControllerList.addElement(c);
		} else {
		    // Notify the play thread which could still be waiting.
		    synchronized (this) {
			prefetchFailed = true;
			notifyAll();
		    }
		    return false;
                }
	    }
	}
	if (removedControllerList != null) {
	  i = removedControllerList.size();
	  while (--i >= 0) {
	    Object  o =  removedControllerList.elementAt(i);
	    controllerList.removeElement(o);
	    ( (BasicController) o).close();
	    if (! deviceBusy((BasicController) o)) {
		// Notify the play thread which could still be waiting.
		synchronized (this) {
		    prefetchFailed = true;
		    notifyAll();
		}
		return false; // prefetch failed
	    }
	  }
	  removedControllerList.removeAllElements();
	  //$ System.err.println("final list of controllers: " + list);
	}

	return true;
    }

    /**
     * Called when the prefetch() is aborted, i.e. deallocate() was called
     * while prefetching.  Release all resources claimed previously by the
     * prefetch call.
     */
    protected final void abortPrefetch() {
	if (controllerList != null) {
	    int i = controllerList.size();
	    while (--i >= 0) {
		Controller c = (Controller) controllerList.elementAt(i);
		c.deallocate();
	    }
	}
	synchronized(this) {
	    notify();
	}
    }

    /**
     * Check the given controller to see if it's busy or not. 
     * Needs to be overridden by subclass.
     * The subclass method should change the master timebase
     * if necessary. It should handle audio only or video
     * only tracks properly when the device is busy.
     * @return true if the given controller is usable; false if the controller
     *  cannot be used.
     */
    protected boolean deviceBusy(BasicController mc) {
	return true;
    }

    /**
     * Slave all the controllers to the master time base.
     * The controllers should be in realized or greater state
     * This differs from the setTimeBase() as it loops through each
     * controllers and call setTimeBase on each of them.
     * @param tb the time base to be used by all controllers.
     * @exception IncompatibleTimeBaseException thrown if any controller 
     * will not accept the player's timebase.
     */
    protected void slaveToMasterTimeBase(TimeBase tb)
	 	throws javax.media.IncompatibleTimeBaseException {

	//$$ System.out.println("slaveToMasterTimeBase: master timebase is " + tb);
	//$ System.out.println("Setting master " + tb + " on " + this);
	this.setTimeBase(tb); // For the player
    }

    private /*synchronized*/ void notifyIfAllEventsArrived(Vector controllerList,
						       Vector receivedEventList) {
	if ( (receivedEventList != null) &&
	     (receivedEventList.size() == currentControllerList.size()) ) {
	    receivedAllEvents = true;
	    resetReceivedEventList(); 		// Reset list of received events
	    synchronized(this) {
		notifyAll();
	    }
	}
    }


    protected /*synchronized*/ void processEvent(ControllerEvent evt) {

	Controller source = evt.getSourceController();

	if (evt instanceof AudioDeviceUnavailableEvent) {
	    sendEvent(new AudioDeviceUnavailableEvent(this));
	    return;
	}

	// If this is a closed event triggered by one of the
	// managed controllers, not triggered by the player,
	// then we'll need to programmtically close all the
	// controllers and the player itself.
	if ( evt instanceof ControllerClosedEvent && !closing &&
	     controllerList.contains(source) &&
	     ! (evt instanceof ResourceUnavailableEvent) ) {

	    // The source of the error event should have been closed
	    // already.  So we'll just remove it from the list of
	    // managed controllers.
	    controllerList.removeElement(source);

	    if (evt instanceof ControllerErrorEvent)
	    	sendEvent(new ControllerErrorEvent(this,
                          ((ControllerErrorEvent) evt).getMessage()));
	    close();
	}

	//
	// Send SizeChangeEvent down to Player
	//
	if ( (evt instanceof SizeChangeEvent) && controllerList.contains(source) ) {
            // System.err.println("width = " +  ((SizeChangeEvent)evt).getWidth());
            // System.err.println("height = " +  ((SizeChangeEvent)evt).getHeight());
            sendEvent(new SizeChangeEvent(this,
                                    ((SizeChangeEvent)evt).getWidth(),
                                    ((SizeChangeEvent)evt).getHeight(),
                                    ((SizeChangeEvent)evt).getScale()));
	    return;
        }


 	// 
	// Send UnsupportedFormatEvent down to Player
	// 
/*
	if ( (evt instanceof UnsupportedFormatEvent) && 
		controllerList.contains(source) ) {
            // System.err.println("Reason = " +  ((UnsupportedFormatEvent)evt).toString());
            sendEvent(new UnsupportedFormatEvent(this, 
			((UnsupportedFormatEvent)evt).getFormat()));
            return;
        }
*/


	// If we get a DurationUpdateEvent from one of the controllers,
	// update the duration of the player
	if ((evt instanceof DurationUpdateEvent) && controllerList.contains(source)) {
	    updateDuration();
	    return;
	}

	// HANGS.
// 	if ((evt instanceof RestartingEvent) && controllerList.contains(source)) {
// 	    System.out.println("MP: Got RestartingEvent from " + source);
// 	    stop(LOCAL_STOP); // Stop without sending any stop event
// 	    sendEvent(new RestartingEvent(this, Started, Prefetching, Started,
// 					  getMediaTime()));
// 	}

	// So I am handling RestartingEvent this way
	if ((evt instanceof RestartingEvent) && controllerList.contains(source)) {
	    restartFrom = source;
	    int i = controllerList.size();
	    super.stop(); // Added
	    setTargetState(Prefetched); // necessary even if super.stop is called.

	    for (int ii = 0; ii < i; ii++) {
		Controller c = (Controller) controllerList.elementAt(ii);
		if (c != source) {
		    c.stop();
		}
	    }
	    super.stop();
	    //	    doStop(); // Allow subclasses to extend the behavior
	    sendEvent(new RestartingEvent(this, Started, Prefetching, Started,
					  getMediaTime()));
	}


	if ((evt instanceof StartEvent) && (source == restartFrom) ) {
	    restartFrom = null;
	    // $$ TODO: Should probably send PrefetchCompleteEvent
	    start();
	}

	if ( (evt instanceof SeekFailedEvent) && controllerList.contains(source) ) {

	    int i = controllerList.size();
	    super.stop(); // Added
	    setTargetState(Prefetched); // necessary even if super.stop is called.

	    for (int ii = 0; ii < i; ii++) {
		Controller c = (Controller) controllerList.elementAt(ii);
		if (c != source) {
		    c.stop();
		}
	    }
	    /*
	    super.stop();
	    setMediaTime(new Time(0));
	    start();
	    */
	    sendEvent(new SeekFailedEvent(this, Started, Prefetched, Prefetched,
					  getMediaTime()));
	}

	if ( (evt instanceof EndOfMediaEvent) && controllerList.contains(source) ) {

	    if (eomEventsReceivedFrom.contains(source)) {
		return;
	    }

	    eomEventsReceivedFrom.addElement(source);
	    if (eomEventsReceivedFrom.size() == controllerList.size()) {
		super.stop();
		sendEvent(new EndOfMediaEvent(this, Started, Prefetched,
					      getTargetState(), getMediaTime()));
	    }
	    return;
	}

	if ((evt instanceof StopAtTimeEvent) && controllerList.contains(source) &&
	    (getState() == Started)) {

	  synchronized (stopAtTimeReceivedFrom) {

	    if (stopAtTimeReceivedFrom.contains(source))
		return;

	    stopAtTimeReceivedFrom.addElement(source);

	    boolean allStopped = (stopAtTimeReceivedFrom.size() == controllerList.size());

	    if (!allStopped) {
		// Now check if the other controllers have already EOM'ed.
		allStopped = true;
		for (int i = 0; i < controllerList.size(); i++) {
		    Controller c = (Controller)controllerList.elementAt(i);
		    if (!stopAtTimeReceivedFrom.contains(c) &&
			!eomEventsReceivedFrom.contains(c)) {
			allStopped = false;
			break;
		    }
		}
	    }

	    if (allStopped) {
		super.stop();
		doSetStopTime(Clock.RESET);
		sendEvent(new StopAtTimeEvent(this, Started, Prefetched,
					      getTargetState(), getMediaTime()));
	    } 
	    return;

	  } // synchronized stopAtTimeReceivedFrom
	}


	if ((evt instanceof CachingControlEvent) && controllerList.contains(source) ) {
	    CachingControl mcc = (CachingControl) 
				((CachingControlEvent) evt).getCachingControl();
	    sendEvent(new CachingControlEvent(this, 
				mcc, 
				mcc.getContentProgress()));
	    return;
	} 

	Vector eventList = potentialEventsList;

	if (controllerList != null && controllerList.contains(source) &&
	    eventList != null && eventList.contains(evt.getClass().getName())) {
	    updateReceivedEventsList(evt);
	    notifyIfAllEventsArrived(controllerList, getReceivedEventsList());
	}
    }

    private boolean trySetRate(float rate) {
	int i = controllerList.size();

	while(--i >=0) {
	    Controller c = (Controller)controllerList.elementAt(i);
	    if ( c.setRate(rate) != rate ) {
		return false;
	    }
	}
	return true;
    }

    protected float doSetRate(float factor) {
	return factor;
    }
    
    /**
     * Set the playback rate on the player.
     * It loops through its list of controllers and invoke setRate on each
     * of them.
     */
    public float setRate(float rate) {
	if (state < Realized) {
	    throwError(new NotRealizedError("Cannot set rate on an unrealized Player."));
	}

	// Verify to see if the DataSource supports that rate.
	if (source instanceof RateConfigureable)
	    rate = checkRateConfig((RateConfigureable)source, rate);

	float oldRate = getRate();

	if (oldRate == rate)
	    return rate;
	
	if (getState() == Controller.Started) {
	    aboutToRestart = true;
	    stop(RESTARTING);
	}
	
	float rateSet; // Actual rate set
	if (!trySetRate(rate)) {
	    if (!trySetRate(oldRate)) { // try to go back to the oldRate
		trySetRate(1.0F); // try setRate(1.0) which shouldn't fail
		rateSet = 1.0F;
	    } else {
		rateSet = oldRate;
	    }
	} else {
	    rateSet = rate;
	}
	super.setRate(rateSet);

	if (aboutToRestart) {
	    syncStart(getTimeBase().getTime());
	    aboutToRestart = false;
	}
	return rateSet;
    }

    /**
     * Check if the given rate configureable supports the given rate.
     * if not, returns the closest match.
     */
    float checkRateConfig(RateConfigureable rc, float rate) {
	RateConfiguration config[] = rc.getRateConfigurations();
	if (config == null)
	    return 1.0f;

	RateConfiguration c;
	RateRange rr;
	float corrected = 1.0f;
	for (int i = 0; i < config.length; i++) {
	    rr = config[i].getRate();
	    if (rr != null && rr.inRange(rate)) {
		rr.setCurrentRate(rate);
		corrected = rate;
		c = rc.setRateConfiguration(config[i]);
		if (c != null && (rr = c.getRate()) != null)
		    corrected = rr.getCurrentRate();	
		break;
	    }
	}
	return corrected;
    }

    /**
     * This is being called from a looping thread to update the stats.
     */
    abstract public void updateStats();


    /*************************************************************************
     * INNER CLASSES 
     *************************************************************************/
}

// PlayThread and StatsThread are no longer inner classes
class PlayThread extends MediaThread {
    BasicPlayer player;
    
    public PlayThread(BasicPlayer player) {
	this.player = player;
	setName(getName() + " (PlayThread)");
	useControlPriority();
    }
    
    public void run() {
	player.play();
    }
}

class StatsThread extends LoopThread {
    BasicPlayer player;
    int pausecount = -1;

    public StatsThread(BasicPlayer p) {
	this.player = p;
    }
    
    protected boolean process() {
	try {
	    Thread.currentThread().sleep(1000);
	} catch (Exception e) {}
	
	// Check to see if the thread was killed.
	// If so exits.
	if (!waitHereIfPaused())
	    return false;
	
	if ( player.getState() == Controller.Started ) {
	  pausecount = -1;
	  player.updateStats();
	} else if ( pausecount < 5 ) {
	  pausecount++;
	  player.updateStats();
	} 
	  
	return true;
    }

}

